//License
/***
 * Java Modbus Library (jamod)
 * Copyright (c) 2002-2004, jamod development team
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the author nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS
 * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 ***/
package net.wimpi.modbus.io;

import net.wimpi.modbus.Modbus;
import net.wimpi.modbus.ModbusIOException;
import net.wimpi.modbus.msg.ModbusMessage;
import net.wimpi.modbus.msg.ModbusRequest;
import net.wimpi.modbus.msg.ModbusResponse;
import net.wimpi.modbus.util.ModbusUtil;
import net.wimpi.modbus.ModbusCoupler;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import java.util.Date;

/**
 * Class that implements the ModbusRTU transport
 * flavor.
 *
 * @author John Charlton
 * @author Dieter Wimberger
 *
 * @version 1.2rc1 (09/11/2004)
 */
public class ModbusRTUTransport
    extends ModbusSerialTransport {

  private InputStream m_InputStream;    //wrap into filter input
  private OutputStream m_OutputStream;      //wrap into filter output

  private byte[] m_InBuffer;
  private BytesInputStream m_ByteIn;         //to read message from
  private BytesOutputStream m_ByteInOut;     //to buffer message to
  private BytesOutputStream m_ByteOut;      //write frames
  private byte[] lastRequest = null;

  public void writeMessage(ModbusMessage msg) throws ModbusIOException {
    try {
      int len;
      synchronized (m_ByteOut) {
        // first clear any input from the receive buffer to prepare
        // for the reply since RTU doesn't have message delimiters
        clearInput();
        //write message to byte out
        m_ByteOut.reset();
        msg.setHeadless();
        msg.writeTo(m_ByteOut);
        len = m_ByteOut.size();
        int[] crc = ModbusUtil.calculateCRC(m_ByteOut.getBuffer(), 0, len);
        m_ByteOut.writeByte(crc[0]);
        m_ByteOut.writeByte(crc[1]);
        //write message
        len = m_ByteOut.size();
        byte buf[] = m_ByteOut.getBuffer();
        m_OutputStream.write(buf, 0, len);     //PDU + CRC
        m_OutputStream.flush();
        if(Modbus.debug) System.out.println("Sent: " + ModbusUtil.toHex(buf, 0, len));
        // clears out the echoed message
        // for RS485
        if (m_Echo) {
          readEcho(len);
        }
        lastRequest = new byte[len];
        System.arraycopy(buf, 0, lastRequest, 0, len);
      }

    } catch (Exception ex) {
      throw new ModbusIOException("I/O failed to write");
    }

  }//writeMessage

  //This is required for the slave that is not supported
  public ModbusRequest readRequest() throws ModbusIOException {
//    throw new RuntimeException("Operation not supported.");
  ModbusRequest request = null;  
  
    boolean done = false;
    long startmillis, startnano;
    long endmillis, endnano;

    int in = -1;
    int[] crcbuffer;
    
    try {

      do {
        
     /* */   
     /*Waiting the first byte */
    
      while ((in = m_InputStream.read()) == -1) {
        
      }

      startnano = System.nanoTime();
      startmillis = System.currentTimeMillis();
      /* synchronization the reading buffer */
      synchronized (m_InBuffer) {
      m_ByteInOut.reset();
      m_ByteInOut.writeByte(in);
      while ((in = m_InputStream.read()) != -1) {
            m_ByteInOut.writeByte(in);
          }
      }
    
      endmillis = System.currentTimeMillis();
      endnano = System.nanoTime();
      /* checksum calculate from the InBuffer len-2 bytes */
      crcbuffer = ModbusUtil.calculateCRC(m_InBuffer,0,m_ByteInOut.size()-2);      
      
       if (Modbus.debug) {           
          Date StartDate = new Date(startmillis);
          Date EndDate = new Date(endmillis);
            byte[] buf = m_ByteInOut.getBuffer();
            int len = m_ByteInOut.size();

            System.out.print("Elapsed nanotime = ");            
            System.out.println(endnano - startnano);
            System.out.print("Elapsed millitime = ");            
            System.out.println(endmillis - startmillis);
            
            System.out.print("\nReading RTU Request \"");
            System.out.print(ModbusUtil.toHex(buf, 0, len));
            System.out.printf("\"\nCalculated CRC = %X %X\n", 
                    crcbuffer[0], crcbuffer[1]);
            //CRC for binary communication
        }                  

       if (!(m_InBuffer[m_ByteInOut.size()-2] == (byte)crcbuffer[0] //low byte first
            &&  m_InBuffer[m_ByteInOut.size()-1] == (byte)crcbuffer[1] )) 
       {
        continue;
       }       
  
       m_ByteIn.reset(m_InBuffer, m_ByteInOut.size());
       in = m_ByteIn.readUnsignedByte();
       //check message with this slave unit identifier
       int UID = ModbusCoupler.getReference().getUnitID();
       if (Modbus.debug) {
        System.out.printf("Unit ID readed = %d, getted = %d\n", in, UID);       
       }           
          
       if (in != ModbusCoupler.getReference().getUnitID()) {
        continue;
       }
       
       in = m_ByteIn.readUnsignedByte();
       //create request
       request = ModbusRequest.createModbusRequest(in);
       request.setHeadless();
       //read message
       m_ByteIn.reset(m_InBuffer, m_ByteInOut.size());
       request.readFrom(m_ByteIn); 
       done = true;
      } while (!done);
      } catch (Exception ex) {
      if(Modbus.debug) System.out.println(ex.getMessage());
      throw new ModbusIOException("I/O exception - failed to read.");
    }

  return request;  
    
  } //readRequest

  /**
   * Clear the input if characters are found in the input stream.
   *
   * @throws ModbusIOException
   */
  public void clearInput() throws IOException {
    if (m_InputStream.available() > 0) {
      int len = m_InputStream.available();
      byte buf[] = new byte[len];
      m_InputStream.read(buf, 0, len);
      if(Modbus.debug) System.out.println("Clear input: " +
                  ModbusUtil.toHex(buf, 0, len));
    }
  }//cleanInput

  public ModbusResponse readResponse()
      throws ModbusIOException {

    boolean done = false;
    ModbusResponse response = null;
    int dlength = 0;

    try {
      do {
        //1. read to function code, create request and read function specific bytes
        synchronized (m_ByteIn) {
          int uid = m_InputStream.read();
          if (uid != -1) {
            int fc = m_InputStream.read();
            m_ByteInOut.reset();
            m_ByteInOut.writeByte(uid);
            m_ByteInOut.writeByte(fc);

            //create response to acquire length of message
            response = ModbusResponse.createModbusResponse(fc);
            response.setHeadless();

            // With Modbus RTU, there is no end frame.  Either we assume
            // the message is complete as is or we must do function
            // specific processing to know the correct length.  To avoid
            // moving frame timing to the serial input functions, we set the
            // timeout and to message specific parsing to read a response.
            getResponse(fc, m_ByteInOut);
            dlength = m_ByteInOut.size() - 2; // less the crc
            if (Modbus.debug) System.out.println("Response: " +
               ModbusUtil.toHex(m_ByteInOut.getBuffer(), 0, dlength + 2));

            m_ByteIn.reset(m_InBuffer, dlength);

            //check CRC
            int[] crc = ModbusUtil.calculateCRC(m_InBuffer, 0, dlength); //does not include CRC
            if (ModbusUtil.unsignedByteToInt(m_InBuffer[dlength]) != crc[0]
                && ModbusUtil.unsignedByteToInt(m_InBuffer[dlength + 1]) != crc[1]) {
              throw new IOException("CRC Error in received frame: " + dlength + " bytes: " + ModbusUtil.toHex(m_ByteIn.getBuffer(), 0, dlength));
            }
          } else {
            throw new IOException("Error reading response");
          }

          //read response
          m_ByteIn.reset(m_InBuffer, dlength);
          if (response != null) {
            response.readFrom(m_ByteIn);
          }
          done = true;
        }//synchronized
      } while (!done);
      return response;
    } catch (Exception ex) {
      System.err.println("Last request: " + ModbusUtil.toHex(lastRequest));
      System.err.println(ex.getMessage());
      throw new ModbusIOException("I/O exception - failed to read");
    }
  }//readResponse

  /**
   * Prepares the input and output streams of this
   * <tt>ModbusRTUTransport</tt> instance.
   *
   * @param in the input stream to be read from.
   * @param out the output stream to write to.
   * @throws IOException if an I\O error occurs.
   */
  public void prepareStreams(InputStream in, OutputStream out)
      throws IOException {
    m_InputStream = in;   //new RTUInputStream(in);
    m_OutputStream = out;

    m_ByteOut = new BytesOutputStream(Modbus.MAX_MESSAGE_LENGTH);
    m_InBuffer = new byte[Modbus.MAX_MESSAGE_LENGTH];
    m_ByteIn = new BytesInputStream(m_InBuffer);
    m_ByteInOut = new BytesOutputStream(m_InBuffer);
  } //prepareStreams

  public void close() throws IOException {
    m_InputStream.close();
    m_OutputStream.close();
  }//close

  private void getResponse(int fn, BytesOutputStream out)
    throws IOException {
    int bc = -1, bc2 = -1, bcw = -1;
    int inpBytes = 0;
    byte inpBuf[] = new byte[256];

    try {
      switch (fn) {
        case 0x01:
        case 0x02:
        case 0x03:
        case 0x04:
        case 0x0C:
        case 0x11:  // report slave ID version and run/stop state
        case 0x14:  // read log entry (60000 memory reference)
        case 0x15:  // write log entry (60000 memory reference)
        case 0x17:
          // read the byte count;
          bc = m_InputStream.read();
          out.write(bc);
          // now get the specified number of bytes and the 2 CRC bytes
          setReceiveThreshold(bc+2);
          inpBytes = m_InputStream.read(inpBuf, 0, bc+2);
          out.write(inpBuf, 0, inpBytes);
          m_CommPort.disableReceiveThreshold();
          if (inpBytes != bc+2) {
            System.out.println("Error: looking for " + (bc+2) +
                               " bytes, received " + inpBytes);
          }
          break;
        case 0x05:
        case 0x06:
        case 0x0B:
        case 0x0F:
        case 0x10:
          // read status: only the CRC remains after address and function code
          setReceiveThreshold(6);
          inpBytes = m_InputStream.read(inpBuf, 0, 6);
          out.write(inpBuf, 0, inpBytes);
          m_CommPort.disableReceiveThreshold();
          break;
        case 0x07:
        case 0x08:
          // read status: only the CRC remains after address and function code
          setReceiveThreshold(3);
          inpBytes = m_InputStream.read(inpBuf, 0, 3);
          out.write(inpBuf, 0, inpBytes);
          m_CommPort.disableReceiveThreshold();
          break;
        case 0x16:
          // eight bytes in addition to the address and function codes
          setReceiveThreshold(8);
          inpBytes = m_InputStream.read(inpBuf, 0, 8);
          out.write(inpBuf, 0, inpBytes);
          m_CommPort.disableReceiveThreshold();
          break;
        case 0x18:
          // read the byte count word
          bc = m_InputStream.read();
          out.write(bc);
          bc2 = m_InputStream.read();
          out.write(bc2);
          bcw = ModbusUtil.makeWord(bc, bc2);
          // now get the specified number of bytes and the 2 CRC bytes
          setReceiveThreshold(bcw+2);
          inpBytes = m_InputStream.read(inpBuf, 0, bcw + 2);
          out.write(inpBuf, 0, inpBytes);
          m_CommPort.disableReceiveThreshold();
          break;
      }
    } catch (IOException e) {
      m_CommPort.disableReceiveThreshold();
      throw new IOException("getResponse serial port exception");
    }
  }//getResponse

  /**
   * Defines the RTU minimum overtime in 10 msec.
   */
  public static final long MIN_OVERTIME = 10;  
  
} //ModbusRTUTransport
